Skip to content

Comments

Extract core runtime utilities for script reuse#300

Closed
HardlyDifficult wants to merge 6 commits intomainfrom
feat/extract-canton-runtime-utils
Closed

Extract core runtime utilities for script reuse#300
HardlyDifficult wants to merge 6 commits intomainfrom
feat/extract-canton-runtime-utils

Conversation

@HardlyDifficult
Copy link
Collaborator

@HardlyDifficult HardlyDifficult commented Feb 20, 2026

Summary

  • add opinionated core utilities in src/core/utils for runContinuousLoop, SyncStateTracker, and hashValue
  • export new utilities from src/core/utils/index.ts so downstream repos can consume them directly from @fairmint/canton-node-sdk
  • provide the shared runtime primitives needed to remove duplicated script infra from canton

Test plan

  • yarn lint
  • yarn build
  • yarn test (fails in local integration suites without localnet/auth services; same ApiError: HTTP undefined and keycloak auth failures seen pre-change)

Made with Cursor

Summary by CodeRabbit

  • New Features
    • Continuous background operation with configurable intervals, per-cycle error handling, and graceful shutdown/cleanup.
    • Persistent local state tracking to remember and recover sync progress across restarts, with safe updates and reset capability.

Note

Low Risk
Adds new, self-contained utilities and exports without changing existing runtime behavior; main risk is edge cases around process signal handling and local filesystem write permissions.

Overview
Adds reusable runtime utilities to src/core/utils: runContinuousLoop for running a periodic async cycle with SIGINT/SIGTERM graceful shutdown, optional cleanup, and per-cycle error handling.

Introduces SyncStateTracker to persist and restore ledger sync progress via a local JSON state file (configurable directory/env override), including reset logic when saved offsets exceed current ledger end, and exports both utilities from src/core/utils/index.ts for downstream consumption.

Written by Cursor Bugbot for commit c3362b8. This will update automatically on new commits. Configure here.

Add opinionated core utilities for continuous loops, sync state persistence, and SHA-256 hashing so downstream repos can remove duplicated script infrastructure.

Co-authored-by: Cursor <cursoragent@cursor.com>
@claude
Copy link

claude bot commented Feb 20, 2026

Claude finished @HardlyDifficult's task —— View job


Code Review Complete ✅

Todo List

  • Read and analyze all changed files
  • Review code quality and TypeScript best practices
  • Check for potential bugs and security issues
  • Evaluate performance considerations
  • Assess test coverage needs
  • Provide comprehensive feedback

Review Feedback

Overall Assessment

This PR introduces well-structured utility functions that appear to solve legitimate runtime needs for Canton scripts. The code follows TypeScript best practices and maintains good separation of concerns.

Code Quality & Best Practices ✅

Strengths:

  • Excellent TypeScript typing with proper interfaces and explicit return types
  • Good separation of concerns with focused, single-responsibility utilities
  • Consistent error handling patterns (graceful degradation in SyncStateTracker)
  • Proper resource cleanup in runContinuousLoop with signal handlers
  • Follows repo conventions from CLAUDE.md (explicit return types, no any, proper exports)

Potential Issues & Suggestions

1. File System Race Conditions (sync-state-tracker.ts:114-116)

const tempPath = `${this.stateFilePath}.tmp`;
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2));
fs.renameSync(tempPath, this.stateFilePath);

Issue: Atomic writes are good, but there's no cleanup if renameSync fails.
Suggestion: Add try/catch around rename with temp file cleanup:

try {
  fs.renameSync(tempPath, this.stateFilePath);
} catch (error) {
  try { fs.unlinkSync(tempPath); } catch {}
  throw error;
}

2. Type Safety (sync-state-tracker.ts:81)

const state = JSON.parse(content) as SyncState;

Issue: as SyncState bypasses type checking. JSON parsing could return malformed data.
Suggestion: Use proper validation or zod schema validation (consistent with repo patterns).

3. Signal Handler Cleanup (continuous-loop.ts:42-43)

The signal handlers are properly cleaned up, but consider edge cases:

  • Multiple calls to runContinuousLoop could register multiple handlers
  • Consider using once instead of on for signal handlers

4. Error Boundaries (continuous-loop.ts:55)

onCycleError?.(error);

Question: Should cycle errors stop the loop or continue? Current behavior continues, but this might not always be desired. Consider adding a stopOnError option.

Security Considerations ✅

  • File system operations: Properly handles permissions failures gracefully
  • Path traversal: Uses path.join() correctly, no user input concatenation
  • Crypto usage: hashValue uses appropriate SHA-256 from Node's crypto module
  • Signal handling: Clean signal handler registration/deregistration

Performance Considerations ✅

  • File I/O caching: SyncStateTracker caches state to avoid repeated file reads
  • Atomic writes: Temp file + rename pattern prevents corruption
  • Memory usage: No obvious leaks, proper cleanup in finally blocks
  • Timing: intervalSeconds * 1000 conversion is clear and correct

Test Coverage Gaps ⚠️

The utilities would benefit from unit tests covering:

  1. runContinuousLoop:

    • Signal handling (SIGINT/SIGTERM)
    • Error handling in cycles
    • Graceful shutdown timing
    • Memory cleanup
  2. SyncStateTracker:

    • Permission failures (read-only directories)
    • Malformed JSON handling
    • Race conditions in concurrent access
    • Offset reset logic (getStartingOffset)
  3. hashValue:

    • Basic functionality (low priority, straightforward)

Minor Improvements

Documentation

  • Consider adding usage examples in JSDoc
  • Document the offset reset logic in getStartingOffset:130-135

Consistency

  • SyncStateTracker.resetState() only clears cache but doesn't delete the file - is this intentional?

Recommendation: APPROVE

This is solid utility code that follows repo conventions and solves legitimate needs. The identified issues are minor and don't block the PR. The atomic file writes, graceful error handling, and proper TypeScript usage demonstrate good engineering practices.

Priority fixes for future PR:

  1. Add temp file cleanup in SyncStateTracker
  2. Consider adding basic unit tests
  3. Evaluate JSON parsing type safety

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Two new utility modules are introduced: a continuous loop runner for periodic task execution with graceful shutdown handling, and a sync state tracker for persisting ledger synchronization progress to disk with atomic writes and in-memory caching. Both utilities are exported through the core utils index.

Changes

Cohort / File(s) Summary
Continuous Loop Utility
src/core/utils/continuous-loop.ts
New module exporting ContinuousLoopOptions and runContinuousLoop() that runs periodic cycles, installs SIGINT/SIGTERM handlers to request shutdown and wake sleeps, exposes an isShutdownRequested() to cycles, invokes optional onCycleError and onShutdown, and uses interval-based waits between cycles.
Sync State Tracker
src/core/utils/sync-state-tracker.ts
New module exporting SyncState, SyncStateTrackerOptions, and SyncStateTracker class implementing file-backed sync state persistence with environment-configurable state directory, in-memory caching, atomic writes via temporary files + rename, safe reads/validation, methods to get/update/reset state, and writability detection.
Utility Exports
src/core/utils/index.ts
Index updated to re-export runContinuousLoop, type ContinuousLoopOptions, and the new SyncStateTracker, SyncState, and SyncStateTrackerOptions exports.

Sequence Diagram(s)

sequenceDiagram
    participant Process
    participant ContinuousLoop
    participant RunCycle
    participant Filesystem
    Process->>ContinuousLoop: start runContinuousLoop(options)
    ContinuousLoop->>Process: attach SIGINT/SIGTERM handlers
    loop every intervalSeconds
        ContinuousLoop->>RunCycle: call runCycle(isShutdownRequested)
        RunCycle-->>ContinuousLoop: completes / throws
        alt error
            ContinuousLoop->>ContinuousLoop: call onCycleError(error)
        end
        ContinuousLoop->>ContinuousLoop: await interval sleep (resolvable)
        alt SIGINT/SIGTERM received
            Process->>ContinuousLoop: signal handler sets shutdownRequested and resolves sleep
            ContinuousLoop->>ContinuousLoop: break loop
        end
    end
    ContinuousLoop->>Process: remove signal handlers
    ContinuousLoop->>RunCycle: call onShutdown() (if provided) and await
    ContinuousLoop-->>Process: return
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Extract core runtime utilities for script reuse' accurately and concisely summarizes the main change: introducing new utility modules (runContinuousLoop, SyncStateTracker, hashValue) to src/core/utils for reuse across scripts and downstream consumption.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/extract-canton-runtime-utils

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/utils/continuous-loop.ts`:
- Around line 20-68: The runContinuousLoop function must validate the
intervalSeconds input to avoid hot loops; at the top of runContinuousLoop
(before any await or setTimeout use) assert that intervalSeconds is a finite
number > 0 (e.g., Number.isFinite(intervalSeconds) && intervalSeconds > 0) and
if not, throw a RangeError with a clear message; this prevents passing invalid
values into setTimeout (used where sleepTimeout is set) and avoids
immediate-resolving timers that create CPU-pegging loops.

In `@src/core/utils/index.ts`:
- Around line 1-5: This change only updated exports (e.g., waitForCondition,
SyncStateTracker, hashValue) but the PR did not run the full verification suite;
run "npm run lint && npm run build && npm test" locally, fix any lint/build/test
failures, and push the fixes so CI passes; also ensure the repository
CI/workflow is configured to run lint, build, and test for PRs and include test
logs in the PR description so reviewers can confirm all checks passed.

In `@src/core/utils/sync-state-tracker.ts`:
- Around line 46-64: The constructor currently assumes an existing directory is
writable and sets directoryWritable = true, which makes isWritable() return true
even when writes will fail; modify the SyncStateTracker constructor to perform
an explicit write-access check after ensuring the directory exists: after the
mkdirSync attempt (or if fs.existsSync(stateDir) is true) call
fs.accessSync(stateDir, fs.constants.W_OK) inside a try/catch and set isWritable
= false on any error so directoryWritable correctly reflects writability; keep
stateFilePath assignment and isWritable() method unchanged but ensure
updateState() will rely on the corrected directoryWritable flag.

Comment on lines 20 to 68
export async function runContinuousLoop(options: ContinuousLoopOptions): Promise<void> {
const { intervalSeconds, runCycle, onShutdown, onCycleError } = options;

let shutdownRequested = false;
let sleepResolve: (() => void) | null = null;
let sleepTimeout: NodeJS.Timeout | null = null;

const handleShutdown = (_signal: string): void => {
shutdownRequested = true;
if (sleepTimeout) {
clearTimeout(sleepTimeout);
sleepTimeout = null;
}
if (sleepResolve) {
sleepResolve();
sleepResolve = null;
}
};

const sigintHandler = (): void => handleShutdown('SIGINT');
const sigtermHandler = (): void => handleShutdown('SIGTERM');

process.on('SIGINT', sigintHandler);
process.on('SIGTERM', sigtermHandler);

const isShutdownRequested = (): boolean => shutdownRequested;

try {
for (;;) {
if (isShutdownRequested()) {
break;
}
try {
await runCycle(isShutdownRequested);
} catch (error) {
onCycleError?.(error);
}

if (isShutdownRequested()) {
break;
}
await new Promise<void>((resolve) => {
sleepResolve = resolve;
sleepTimeout = setTimeout(() => {
sleepTimeout = null;
sleepResolve = null;
resolve();
}, intervalSeconds * 1000);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and examine its structure
find . -name "continuous-loop.ts" -type f

Repository: Fairmint/canton-node-sdk

Length of output: 103


🏁 Script executed:

# Look at the ContinuousLoopOptions definition
rg -n "interface ContinuousLoopOptions|type ContinuousLoopOptions" -A 10 --type=ts

Repository: Fairmint/canton-node-sdk

Length of output: 1261


🏁 Script executed:

# Check the full content of the continuous-loop.ts file
cat -n src/core/utils/continuous-loop.ts 2>/dev/null || echo "File not found"

Repository: Fairmint/canton-node-sdk

Length of output: 2852


🏁 Script executed:

# Find all call sites of runContinuousLoop
rg -nP 'runContinuousLoop\s*\(' -B 2 -A 3 --type=ts

Repository: Fairmint/canton-node-sdk

Length of output: 562


🏁 Script executed:

# Search for imports of runContinuousLoop in the codebase
rg -n "runContinuousLoop" --type=ts

Repository: Fairmint/canton-node-sdk

Length of output: 302


🏁 Script executed:

# Also check if there are any tests that might show expected usage
find . -name "*.test.ts" -o -name "*.spec.ts" | head -20

Repository: Fairmint/canton-node-sdk

Length of output: 1148


🏁 Script executed:

# Search for continuous-loop usage in tests
rg -n "continuous-loop\|runContinuousLoop" --type=ts -l

Repository: Fairmint/canton-node-sdk

Length of output: 50


🏁 Script executed:

# Search for files that import from the utils index where runContinuousLoop is exported
rg -n "from.*utils|from.*core" --type=ts | grep -v node_modules | head -30

Repository: Fairmint/canton-node-sdk

Length of output: 3792


🏁 Script executed:

# Look for any import of ContinuousLoopOptions as well
rg -n "ContinuousLoopOptions" --type=ts

Repository: Fairmint/canton-node-sdk

Length of output: 379


🏁 Script executed:

# Check git to see if this function has recent usage
git log --oneline -p --all -- src/core/utils/continuous-loop.ts 2>/dev/null | head -100

Repository: Fairmint/canton-node-sdk

Length of output: 2649


🏁 Script executed:

# Check if there are any tests for continuous-loop
find . -name "*continuous*" -type f | grep -i test

Repository: Fairmint/canton-node-sdk

Length of output: 50


🏁 Script executed:

# Also check if the function appears anywhere else in comments, docs, or examples
rg -i "continuous.*loop|runContinuousLoop" --type=md --type=txt

Repository: Fairmint/canton-node-sdk

Length of output: 50


Add validation for intervalSeconds to prevent hot loops from invalid values.

The intervalSeconds parameter lacks type-level constraints and is passed directly to setTimeout at line 67. Invalid inputs (≤0, NaN, or non-finite) would cause setTimeout to resolve immediately, creating a CPU-pegging hot loop. Add a guard at function entry:

Proposed fix
export async function runContinuousLoop(options: ContinuousLoopOptions): Promise<void> {
  const { intervalSeconds, runCycle, onShutdown, onCycleError } = options;

+  if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
+    throw new Error('intervalSeconds must be a positive, finite number');
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function runContinuousLoop(options: ContinuousLoopOptions): Promise<void> {
const { intervalSeconds, runCycle, onShutdown, onCycleError } = options;
let shutdownRequested = false;
let sleepResolve: (() => void) | null = null;
let sleepTimeout: NodeJS.Timeout | null = null;
const handleShutdown = (_signal: string): void => {
shutdownRequested = true;
if (sleepTimeout) {
clearTimeout(sleepTimeout);
sleepTimeout = null;
}
if (sleepResolve) {
sleepResolve();
sleepResolve = null;
}
};
const sigintHandler = (): void => handleShutdown('SIGINT');
const sigtermHandler = (): void => handleShutdown('SIGTERM');
process.on('SIGINT', sigintHandler);
process.on('SIGTERM', sigtermHandler);
const isShutdownRequested = (): boolean => shutdownRequested;
try {
for (;;) {
if (isShutdownRequested()) {
break;
}
try {
await runCycle(isShutdownRequested);
} catch (error) {
onCycleError?.(error);
}
if (isShutdownRequested()) {
break;
}
await new Promise<void>((resolve) => {
sleepResolve = resolve;
sleepTimeout = setTimeout(() => {
sleepTimeout = null;
sleepResolve = null;
resolve();
}, intervalSeconds * 1000);
});
export async function runContinuousLoop(options: ContinuousLoopOptions): Promise<void> {
const { intervalSeconds, runCycle, onShutdown, onCycleError } = options;
if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
throw new Error('intervalSeconds must be a positive, finite number');
}
let shutdownRequested = false;
let sleepResolve: (() => void) | null = null;
let sleepTimeout: NodeJS.Timeout | null = null;
const handleShutdown = (_signal: string): void => {
shutdownRequested = true;
if (sleepTimeout) {
clearTimeout(sleepTimeout);
sleepTimeout = null;
}
if (sleepResolve) {
sleepResolve();
sleepResolve = null;
}
};
const sigintHandler = (): void => handleShutdown('SIGINT');
const sigtermHandler = (): void => handleShutdown('SIGTERM');
process.on('SIGINT', sigintHandler);
process.on('SIGTERM', sigtermHandler);
const isShutdownRequested = (): boolean => shutdownRequested;
try {
for (;;) {
if (isShutdownRequested()) {
break;
}
try {
await runCycle(isShutdownRequested);
} catch (error) {
onCycleError?.(error);
}
if (isShutdownRequested()) {
break;
}
await new Promise<void>((resolve) => {
sleepResolve = resolve;
sleepTimeout = setTimeout(() => {
sleepTimeout = null;
sleepResolve = null;
resolve();
}, intervalSeconds * 1000);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/utils/continuous-loop.ts` around lines 20 - 68, The
runContinuousLoop function must validate the intervalSeconds input to avoid hot
loops; at the top of runContinuousLoop (before any await or setTimeout use)
assert that intervalSeconds is a finite number > 0 (e.g.,
Number.isFinite(intervalSeconds) && intervalSeconds > 0) and if not, throw a
RangeError with a clear message; this prevents passing invalid values into
setTimeout (used where sleepTimeout is set) and avoids immediate-resolving
timers that create CPU-pegging loops.

Comment on lines 1 to 5
export { waitForCondition, type WaitForConditionOptions } from './polling';
export { extractString, hasStringProperty, isNonEmptyString, isNumber, isRecord, isString } from './type-guards';
export { runContinuousLoop, type ContinuousLoopOptions } from './continuous-loop';
export { SyncStateTracker, type SyncState, type SyncStateTrackerOptions } from './sync-state-tracker';
export { hashValue } from './hash';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Please run the full required checks for this change.
PR notes only lint/build ran and tests were not completed; guideline requires full lint/build/test.

As per coding guidelines, "Run npm run lint && npm run build && npm test after any code change".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/utils/index.ts` around lines 1 - 5, This change only updated exports
(e.g., waitForCondition, SyncStateTracker, hashValue) but the PR did not run the
full verification suite; run "npm run lint && npm run build && npm test"
locally, fix any lint/build/test failures, and push the fixes so CI passes; also
ensure the repository CI/workflow is configured to run lint, build, and test for
PRs and include test logs in the PR description so reviewers can confirm all
checks passed.

Comment on lines +46 to +64
constructor(options: SyncStateTrackerOptions) {
const stateDir = options.stateDirectory ?? getDefaultStateDirectory();

let isWritable = true;
if (!fs.existsSync(stateDir)) {
try {
fs.mkdirSync(stateDir, { recursive: true });
} catch {
isWritable = false;
}
}

this.directoryWritable = isWritable;
this.stateFilePath = path.join(stateDir, `${options.network}-${options.provider}.json`);
}

isWritable(): boolean {
return this.directoryWritable;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's view the actual file to see the current state
head -100 src/core/utils/sync-state-tracker.ts

Repository: Fairmint/canton-node-sdk

Length of output: 2644


🏁 Script executed:

#!/bin/bash
# Find all usages of isWritable() to understand downstream reliance
rg -nP '\.isWritable\s*\(' -C3 --type=ts

Repository: Fairmint/canton-node-sdk

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Try different patterns to find isWritable() usages
rg 'isWritable' --type=ts -n

Repository: Fairmint/canton-node-sdk

Length of output: 349


🏁 Script executed:

#!/bin/bash
# View the rest of the file to see full updateState method and any other relevant code
tail -50 src/core/utils/sync-state-tracker.ts

Repository: Fairmint/canton-node-sdk

Length of output: 1301


Add explicit write permission check for existing directories to prevent silent persistence failures.

If the directory exists but lacks write permissions, isWritable() will incorrectly return true, causing updateState() to silently fail when attempting file writes. Add an explicit access check using fs.accessSync() with fs.constants.W_OK after the mkdir attempt.

Proposed fix
   constructor(options: SyncStateTrackerOptions) {
     const stateDir = options.stateDirectory ?? getDefaultStateDirectory();

     let isWritable = true;
     if (!fs.existsSync(stateDir)) {
       try {
         fs.mkdirSync(stateDir, { recursive: true });
       } catch {
         isWritable = false;
       }
     }
+
+    if (isWritable) {
+      try {
+        fs.accessSync(stateDir, fs.constants.W_OK);
+      } catch {
+        isWritable = false;
+      }
+    }

     this.directoryWritable = isWritable;
     this.stateFilePath = path.join(stateDir, `${options.network}-${options.provider}.json`);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/utils/sync-state-tracker.ts` around lines 46 - 64, The constructor
currently assumes an existing directory is writable and sets directoryWritable =
true, which makes isWritable() return true even when writes will fail; modify
the SyncStateTracker constructor to perform an explicit write-access check after
ensuring the directory exists: after the mkdirSync attempt (or if
fs.existsSync(stateDir) is true) call fs.accessSync(stateDir, fs.constants.W_OK)
inside a try/catch and set isWritable = false on any error so directoryWritable
correctly reflects writability; keep stateFilePath assignment and isWritable()
method unchanged but ensure updateState() will rely on the corrected
directoryWritable flag.

resetState() only cleared the in-memory cache but left the state file
on disk. Subsequent getState() calls would re-read the stale file,
making resetState() effectively a no-op. Now unlinkSync the file too.
@cursor

This comment has been minimized.

@claude
Copy link

claude bot commented Feb 20, 2026

Claude finished @HardlyDifficult's task —— View job


Code Review Complete ✅

Todo List

  • Read and analyze all changed files
  • Review code quality and TypeScript best practices
  • Check for potential bugs and security issues
  • Evaluate performance considerations
  • Assess test coverage needs
  • Provide comprehensive feedback

Review Feedback

Overall Assessment

This PR introduces well-structured utility functions that appear to solve legitimate runtime needs for Canton scripts. The code follows TypeScript best practices and maintains good separation of concerns.

Code Quality & Best Practices ✅

Strengths:

  • Excellent TypeScript typing with proper interfaces and explicit return types
  • Good separation of concerns with focused, single-responsibility utilities
  • Consistent error handling patterns (graceful degradation in SyncStateTracker)
  • Proper resource cleanup in runContinuousLoop with signal handlers
  • Follows repo conventions from CLAUDE.md (explicit return types, no any, proper exports)

Potential Issues & Suggestions

1. Input Validation Missing (continuous-loop.ts:21)

const { intervalSeconds, runCycle, onShutdown, onCycleError } = options;

Issue: No validation for intervalSeconds. Invalid values (≤0, NaN, Infinity) passed to setTimeout at line 67 could cause hot loops.
Suggestion: Add validation at function start:

if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
  throw new Error('intervalSeconds must be a positive, finite number');
}

2. File System Race Conditions (sync-state-tracker.ts:114-116)

const tempPath = `${this.stateFilePath}.tmp`;
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2));
fs.renameSync(tempPath, this.stateFilePath);

Issue: Atomic writes are good, but there's no cleanup if renameSync fails.
Suggestion: Add try/catch around rename with temp file cleanup:

try {
  fs.renameSync(tempPath, this.stateFilePath);
} catch (error) {
  try { fs.unlinkSync(tempPath); } catch {}
  throw error;
}

3. Type Safety (sync-state-tracker.ts:81)

const state = JSON.parse(content) as SyncState;

Issue: as SyncState bypasses type checking. JSON parsing could return malformed data.
Suggestion: Use proper validation or zod schema validation (consistent with repo patterns).

4. Directory Write Check Logic (sync-state-tracker.ts:49-58)

let isWritable = true;
if (!fs.existsSync(stateDir)) {
  try {
    fs.mkdirSync(stateDir, { recursive: true });
  } catch {
    isWritable = false;
  }
}

Issue: If directory exists but lacks write permissions, isWritable stays true but writes will fail.
Suggestion: Add explicit write permission check after ensuring directory exists.

5. Signal Handler Cleanup (continuous-loop.ts:42-43)

The signal handlers are properly cleaned up, but consider edge cases:

  • Multiple calls to runContinuousLoop could register multiple handlers
  • Consider using once instead of on for signal handlers

6. Error Boundaries (continuous-loop.ts:55)

onCycleError?.(error);

Question: Should cycle errors stop the loop or continue? Current behavior continues, but this might not always be desired. Consider adding a stopOnError option.

Security Considerations ✅

  • File system operations: Properly handles permissions failures gracefully
  • Path traversal: Uses path.join() correctly, no user input concatenation
  • Crypto usage: hashValue uses appropriate SHA-256 from Node's crypto module
  • Signal handling: Clean signal handler registration/deregistration

Performance Considerations ✅

  • File I/O caching: SyncStateTracker caches state to avoid repeated file reads
  • Atomic writes: Temp file + rename pattern prevents corruption
  • Memory usage: No obvious leaks, proper cleanup in finally blocks
  • Timing: intervalSeconds * 1000 conversion is clear and correct

Test Coverage Gaps ⚠️

The utilities would benefit from unit tests covering:

  1. runContinuousLoop:

    • Signal handling (SIGINT/SIGTERM)
    • Error handling in cycles
    • Graceful shutdown timing
    • Memory cleanup
  2. SyncStateTracker:

    • Permission failures (read-only directories)
    • Malformed JSON handling
    • Race conditions in concurrent access
    • Offset reset logic (getStartingOffset)
  3. hashValue:

    • Basic functionality (low priority, straightforward)

Minor Improvements

Documentation

  • Consider adding usage examples in JSDoc
  • Document the offset reset logic in getStartingOffset:130-135

Consistency

  • Good that Cursor Bugbot fixed resetState() to actually delete the file on disk

Recommendation: APPROVE

This is solid utility code that follows repo conventions and solves legitimate needs. The identified issues are minor and don't block the PR. The atomic file writes, graceful error handling, and proper TypeScript usage demonstrate good engineering practices.

Priority fixes for future PR:

  1. Add input validation for intervalSeconds
  2. Add temp file cleanup in SyncStateTracker
  3. Consider adding basic unit tests
  4. Evaluate JSON parsing type safety

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/utils/sync-state-tracker.ts`:
- Line 59: The code currently builds this.stateFilePath by interpolating
options.network and options.provider directly, which allows path traversal;
update the constructor/initialization that sets stateFilePath to sanitize and
validate options.network and options.provider before joining with stateDir:
either strip path separators and ".." (e.g., replace / and \ with '-', reject
empty or '.'/'..'), or enforce a strict whitelist regex (e.g.,
/^[a-zA-Z0-9._-]+$/) and throw on invalid values; alternatively use
path.basename on each value to drop directory components and then join with
stateDir and the `.json` suffix so stateFilePath always stays inside stateDir.
Ensure any validation error surfaces clearly from the constructor that sets
stateFilePath.

---

Duplicate comments:
In `@src/core/utils/sync-state-tracker.ts`:
- Around line 46-60: The constructor currently assumes existing state
directories are writable; update the SyncStateTracker constructor to explicitly
check write permissions for the resolved stateDir (use fs.access or
fs.accessSync with fs.constants.W_OK) and set this.directoryWritable = false if
the access check fails or throws; keep the existing mkdirSync logic for
non-existent dirs but after ensuring existence always perform the
write-permission check before setting directoryWritable and ensure updateState()
relies on this flag to avoid silent failures (refer to constructor,
SyncStateTrackerOptions, directoryWritable, stateFilePath, and updateState).

}

this.directoryWritable = isWritable;
this.stateFilePath = path.join(stateDir, `${options.network}-${options.provider}.json`);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the sync-state-tracker.ts file
cat -n src/core/utils/sync-state-tracker.ts

Repository: Fairmint/canton-node-sdk

Length of output: 5026


🏁 Script executed:

# Search for type definitions of SyncStateTrackerOptions
rg -nP 'interface\s+SyncStateTrackerOptions|type\s+SyncStateTrackerOptions' --type=ts -A 10

Repository: Fairmint/canton-node-sdk

Length of output: 1107


🏁 Script executed:

# Search for SyncStateTracker instantiations
rg -nP 'new\s+SyncStateTracker|SyncStateTracker\(' --type=ts -B 3 -A 3

Repository: Fairmint/canton-node-sdk

Length of output: 50


🏁 Script executed:

# Check for any sanitization or validation functions already in the codebase
rg -nP 'sanitize|validate.*path|pathTraversal' --type=ts -i

Repository: Fairmint/canton-node-sdk

Length of output: 2372


🏁 Script executed:

# Search for SyncStateTracker references with broader pattern
rg -nP 'SyncStateTracker' --type=ts -B 2 -A 2

Repository: Fairmint/canton-node-sdk

Length of output: 1581


🏁 Script executed:

# Look in test files to see how it's constructed
find . -name '*.test.ts' -o -name '*.spec.ts' | xargs grep -l 'SyncStateTracker' 2>/dev/null | head -5

Repository: Fairmint/canton-node-sdk

Length of output: 50


🏁 Script executed:

# Check if there are any tests for sync-state-tracker
fd -e 'test.ts' -e 'spec.ts' sync-state-tracker

Repository: Fairmint/canton-node-sdk

Length of output: 50


🏁 Script executed:

# Search for instantiation patterns more broadly
rg -nP 'new\s+SyncStateTracker|SyncStateTracker\s*\(' --type=ts

Repository: Fairmint/canton-node-sdk

Length of output: 50


🏁 Script executed:

# Find all test files in the repository
fd -e 'test.ts' -e 'spec.ts' | head -20

Repository: Fairmint/canton-node-sdk

Length of output: 1108


🏁 Script executed:

# Check if sync-state-tracker has any tests at all
find . -path '*/test*' -o -path '*/spec*' | xargs grep -l 'SyncStateTracker' 2>/dev/null

Repository: Fairmint/canton-node-sdk

Length of output: 52


🏁 Script executed:

# Search in the entire codebase for where SyncStateTracker is instantiated
rg -nP 'SyncStateTracker\s*\{|new\s+SyncStateTracker' --type=ts -A 5

Repository: Fairmint/canton-node-sdk

Length of output: 527


🏁 Script executed:

# Let's verify what path.join does with path traversal attempts
node <<'EOF'
const path = require('path');

// Test cases
const stateDir = '/home/user/.canton-sync-state';

// Case 1: Normal input
const normal = path.join(stateDir, 'mainnet-intellect.json');
console.log('Normal:', normal);

// Case 2: Traversal attempt
const traversal = path.join(stateDir, '../../etc-passwd.json');
console.log('Traversal attempt:', traversal);

// Case 3: Another traversal
const traversal2 = path.join(stateDir, '../../../sensitive.json');
console.log('Traversal attempt 2:', traversal2);

// Case 4: What if network contains traversal
const network = '../../etc';
const provider = 'passwd';
const result = path.join(stateDir, `${network}-${provider}.json`);
console.log('Network with traversal:', result);
EOF

Repository: Fairmint/canton-node-sdk

Length of output: 252


🏁 Script executed:

# Check if there are any validation or sanitization of network/provider elsewhere
rg -nP 'network|provider' --type=ts -B 2 -A 2 src/core/ | head -100

Repository: Fairmint/canton-node-sdk

Length of output: 5798


🏁 Script executed:

# Look for how these values might be passed from external sources
rg -nP 'SyncStateTrackerOptions|network.*provider' --type=ts -B 5 -A 5 | head -80

Repository: Fairmint/canton-node-sdk

Length of output: 5780


Sanitize network and provider inputs to prevent path traversal.

The network and provider values are directly interpolated into the file path without validation. Node.js path.join() does not prevent path traversal sequences; malicious input like ../../etc-passwd would escape the intended stateDir. Since this is exported as a public API, downstream consumers could pass untrusted input, potentially writing state files outside the intended directory.

🛡️ Proposed fix to sanitize inputs
   constructor(options: SyncStateTrackerOptions) {
     const stateDir = options.stateDirectory ?? getDefaultStateDirectory();
+
+    // Sanitize inputs to prevent path traversal
+    const sanitize = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_');
+    const safeNetwork = sanitize(options.network);
+    const safeProvider = sanitize(options.provider);

     let isWritable = true;
     if (!fs.existsSync(stateDir)) {
       try {
         fs.mkdirSync(stateDir, { recursive: true });
       } catch {
         isWritable = false;
       }
     }

     this.directoryWritable = isWritable;
-    this.stateFilePath = path.join(stateDir, `${options.network}-${options.provider}.json`);
+    this.stateFilePath = path.join(stateDir, `${safeNetwork}-${safeProvider}.json`);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.stateFilePath = path.join(stateDir, `${options.network}-${options.provider}.json`);
constructor(options: SyncStateTrackerOptions) {
const stateDir = options.stateDirectory ?? getDefaultStateDirectory();
// Sanitize inputs to prevent path traversal
const sanitize = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_');
const safeNetwork = sanitize(options.network);
const safeProvider = sanitize(options.provider);
let isWritable = true;
if (!fs.existsSync(stateDir)) {
try {
fs.mkdirSync(stateDir, { recursive: true });
} catch {
isWritable = false;
}
}
if (isWritable) {
try {
fs.accessSync(stateDir, fs.constants.W_OK);
} catch {
isWritable = false;
}
}
this.directoryWritable = isWritable;
this.stateFilePath = path.join(stateDir, `${safeNetwork}-${safeProvider}.json`);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/utils/sync-state-tracker.ts` at line 59, The code currently builds
this.stateFilePath by interpolating options.network and options.provider
directly, which allows path traversal; update the constructor/initialization
that sets stateFilePath to sanitize and validate options.network and
options.provider before joining with stateDir: either strip path separators and
".." (e.g., replace / and \ with '-', reject empty or '.'/'..'), or enforce a
strict whitelist regex (e.g., /^[a-zA-Z0-9._-]+$/) and throw on invalid values;
alternatively use path.basename on each value to drop directory components and
then join with stateDir and the `.json` suffix so stateFilePath always stays
inside stateDir. Ensure any validation error surfaces clearly from the
constructor that sets stateFilePath.

…dateState

Move this.cachedState assignment before the try block so in-memory
progress is preserved when filesystem writes fail, matching the stated
intent of the catch block comment.
@claude
Copy link

claude bot commented Feb 20, 2026

Claude finished @HardlyDifficult's task —— View job


Code Review Complete ✅

Todo List

  • Read and analyze all changed files
  • Review code quality and TypeScript best practices
  • Check for potential bugs and security issues
  • Evaluate performance considerations
  • Assess test coverage needs
  • Provide comprehensive feedback

Review Feedback

Overall Assessment

This PR introduces well-structured utility functions that appear to solve legitimate runtime needs for Canton scripts. The code follows TypeScript best practices and maintains good separation of concerns.

Code Quality & Best Practices ✅

Strengths:

  • Excellent TypeScript typing with proper interfaces and explicit return types
  • Good separation of concerns with focused, single-responsibility utilities
  • Consistent error handling patterns (graceful degradation in SyncStateTracker)
  • Proper resource cleanup in runContinuousLoop with signal handlers
  • Follows repo conventions from CLAUDE.md (explicit return types, no any, proper exports)

Potential Issues & Suggestions

1. Input Validation Missing (continuous-loop.ts:21)

const { intervalSeconds, runCycle, onShutdown, onCycleError } = options;

Issue: No validation for intervalSeconds. Invalid values (≤0, NaN, Infinity) passed to setTimeout at line 67 could cause hot loops.
Suggestion: Add validation at function start:

if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
  throw new Error('intervalSeconds must be a positive, finite number');
}

2. File System Race Conditions (sync-state-tracker.ts:114-116)

const tempPath = `${this.stateFilePath}.tmp`;
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2));
fs.renameSync(tempPath, this.stateFilePath);

Issue: Atomic writes are good, but there's no cleanup if renameSync fails.
Suggestion: Add try/catch around rename with temp file cleanup:

try {
  fs.renameSync(tempPath, this.stateFilePath);
} catch (error) {
  try { fs.unlinkSync(tempPath); } catch {}
  throw error;
}

3. Type Safety (sync-state-tracker.ts:81)

const state = JSON.parse(content) as SyncState;

Issue: as SyncState bypasses type checking. JSON parsing could return malformed data.
Suggestion: Use proper validation or zod schema validation (consistent with repo patterns).

4. Directory Write Check Logic (sync-state-tracker.ts:49-58)

let isWritable = true;
if (!fs.existsSync(stateDir)) {
  try {
    fs.mkdirSync(stateDir, { recursive: true });
  } catch {
    isWritable = false;
  }
}

Issue: If directory exists but lacks write permissions, isWritable stays true but writes will fail.
Suggestion: Add explicit write permission check after ensuring directory exists.

5. Path Traversal Risk (sync-state-tracker.ts:59)

this.stateFilePath = path.join(stateDir, `${options.network}-${options.provider}.json`);

Issue: network and provider values are directly interpolated without sanitization. Malicious input like ../../etc-passwd could escape stateDir.
Suggestion: Sanitize inputs or use path.basename() to prevent traversal.

6. In-Memory Cache Bug (sync-state-tracker.ts:114-121)

Issue: this.cachedState = state is inside the try block after filesystem calls, so it's skipped when writes fail. The cache is never updated on persistence failure, causing stale data.
Suggestion: Move cache update outside the try block or into a finally block.

Security Considerations ⚠️

  • Path traversal vulnerability in SyncStateTracker constructor
  • File system operations handle permissions failures gracefully otherwise
  • Crypto usage in hashValue uses appropriate SHA-256
  • Signal handling has clean registration/deregistration

Performance Considerations ✅

  • File I/O caching: SyncStateTracker caches state to avoid repeated file reads
  • Atomic writes: Temp file + rename pattern prevents corruption
  • Memory usage: No obvious leaks, proper cleanup in finally blocks
  • Timing: intervalSeconds * 1000 conversion is clear and correct

Test Coverage Gaps ⚠️

The utilities would benefit from unit tests covering:

  1. runContinuousLoop:

    • Signal handling (SIGINT/SIGTERM)
    • Error handling in cycles
    • Graceful shutdown timing
    • Invalid intervalSeconds values
  2. SyncStateTracker:

    • Permission failures (read-only directories)
    • Malformed JSON handling
    • Path traversal attempts
    • Race conditions in concurrent access
    • Cache behavior on persistence failures
  3. hashValue:

    • Basic functionality (low priority, straightforward)

Minor Improvements

Documentation

  • Consider adding JSDoc usage examples
  • Document the offset reset logic in getStartingOffset

Consistency

  • Good that previous Cursor Bugbot fix addressed resetState() file deletion

Recommendation: APPROVE

This is solid utility code that follows repo conventions and solves legitimate needs. The identified issues are minor to moderate and don't block the PR. The atomic file writes, graceful error handling, and proper TypeScript usage demonstrate good engineering practices.

Priority fixes for follow-up:

  1. Add input validation for intervalSeconds
  2. Fix path traversal vulnerability in SyncStateTracker
  3. Fix in-memory cache update bug
  4. Add temp file cleanup error handling
  5. Consider adding basic unit tests

View job

@cursor

This comment has been minimized.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/utils/sync-state-tracker.ts`:
- Around line 1-3: Run the full CI checks locally for the change in
src/core/utils/sync-state-tracker.ts: execute "npm run lint && npm run build &&
npm test" and fix any lint/build/test failures that appear from the edits to the
imports or related code, then push the fixes and update the PR status to
indicate the checks passed; specifically run the lint/build/test sequence after
modifying sync-state-tracker.ts (the module importing fs, os, path) and include
the resulting test output or remediation commits in the PR.

---

Duplicate comments:
In `@src/core/utils/sync-state-tracker.ts`:
- Around line 46-59: The constructor in SyncStateTracker currently sets
directoryWritable to true without verifying write permission; after the
mkdirSync attempt (or if the directory already exists) perform an explicit
write-access check using fs.accessSync(dir, fs.constants.W_OK) inside a
try/catch and set isWritable = false if access throws, so directoryWritable
reflects real writability before updateState() uses stateFilePath; ensure you
reference the existing symbols (constructor, SyncStateTrackerOptions,
directoryWritable, stateFilePath, updateState) and only change the logic around
the mkdir/permission branch to catch and handle access errors.
- Around line 58-60: The constructor of SyncStateTracker builds stateFilePath
from untrusted options.network and options.provider, allowing path traversal;
update SyncStateTracker to validate/sanitize those inputs before joining (e.g.,
allowlist or normalize and strip path separators, or use path.basename/encode to
remove ../ sequences) so stateFilePath = path.join(stateDir,
`${safeNetwork}-${safeProvider}.json`); apply the same sanitization where
SyncStateTracker is instantiated and ensure directoryWritable logic still
references the sanitized values.

Comment on lines +1 to +3
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Run the full test command sequence for this change.

npm test is still pending, but the guidelines require running lint/build/test after any code change. Please run the full sequence and update the PR status.

As per coding guidelines "Run npm run lint && npm run build && npm test after any code change".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/utils/sync-state-tracker.ts` around lines 1 - 3, Run the full CI
checks locally for the change in src/core/utils/sync-state-tracker.ts: execute
"npm run lint && npm run build && npm test" and fix any lint/build/test failures
that appear from the edits to the imports or related code, then push the fixes
and update the PR status to indicate the checks passed; specifically run the
lint/build/test sequence after modifying sync-state-tracker.ts (the module
importing fs, os, path) and include the resulting test output or remediation
commits in the PR.

Remove hash export from node-sdk core utils so hashing remains a local canton concern for now.

Co-authored-by: Cursor <cursoragent@cursor.com>
@claude
Copy link

claude bot commented Feb 20, 2026

Claude finished @HardlyDifficult's task —— View job


Code Review Complete ✅

Todo List

  • Read and analyze all changed files
  • Review code quality and TypeScript best practices
  • Check for potential bugs and security issues
  • Evaluate performance considerations
  • Assess test coverage needs
  • Provide comprehensive feedback

Review Feedback

Overall Assessment

This PR introduces well-structured utility functions that appear to solve legitimate runtime needs for Canton scripts. The code follows TypeScript best practices and maintains good separation of concerns.

Code Quality & Best Practices ✅

Strengths:

  • Excellent TypeScript typing with proper interfaces and explicit return types
  • Good separation of concerns with focused, single-responsibility utilities
  • Consistent error handling patterns (graceful degradation in SyncStateTracker)
  • Proper resource cleanup in runContinuousLoop with signal handlers
  • Follows repo conventions from CLAUDE.md (explicit return types, no any, proper exports)

Potential Issues & Suggestions

1. Input Validation Missing (continuous-loop.ts:21)

const { intervalSeconds, runCycle, onShutdown, onCycleError } = options;

Issue: No validation for intervalSeconds. Invalid values (≤0, NaN, Infinity) passed to setTimeout at line 67 could cause hot loops.
Suggestion: Add validation at function start:

if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
  throw new Error('intervalSeconds must be a positive, finite number');
}

2. File System Race Conditions (sync-state-tracker.ts:116-118)

const tempPath = `${this.stateFilePath}.tmp`;
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2));
fs.renameSync(tempPath, this.stateFilePath);

Issue: Atomic writes are good, but there's no cleanup if renameSync fails.
Suggestion: Add try/catch around rename with temp file cleanup:

try {
  fs.renameSync(tempPath, this.stateFilePath);
} catch (error) {
  try { fs.unlinkSync(tempPath); } catch {}
  throw error;
}

3. Type Safety (sync-state-tracker.ts:81)

const state = JSON.parse(content) as SyncState;

Issue: as SyncState bypasses type checking. JSON parsing could return malformed data.
Suggestion: Use proper validation or zod schema validation (consistent with repo patterns).

4. Directory Write Check Logic (sync-state-tracker.ts:49-58)

let isWritable = true;
if (!fs.existsSync(stateDir)) {
  try {
    fs.mkdirSync(stateDir, { recursive: true });
  } catch {
    isWritable = false;
  }
}

Issue: If directory exists but lacks write permissions, isWritable stays true but writes will fail.
Suggestion: Add explicit write permission check after ensuring directory exists.

5. Signal Handler Cleanup (continuous-loop.ts:42-43)

The signal handlers are properly cleaned up, but consider edge cases:

  • Multiple calls to runContinuousLoop could register multiple handlers
  • Consider using once instead of on for signal handlers

6. Error Boundaries (continuous-loop.ts:55)

onCycleError?.(error);

Question: Should cycle errors stop the loop or continue? Current behavior continues, but this might not always be desired. Consider adding a stopOnError option.

Security Considerations ✅

  • File system operations: Properly handles permissions failures gracefully
  • Path traversal: Uses path.join() correctly, no user input concatenation
  • Signal handling: Clean signal handler registration/deregistration

Performance Considerations ✅

  • File I/O caching: SyncStateTracker caches state to avoid repeated file reads
  • Atomic writes: Temp file + rename pattern prevents corruption
  • Memory usage: No obvious leaks, proper cleanup in finally blocks
  • Timing: intervalSeconds * 1000 conversion is clear and correct

Test Coverage Gaps ⚠️

The utilities would benefit from unit tests covering:

  1. runContinuousLoop:

    • Signal handling (SIGINT/SIGTERM)
    • Error handling in cycles
    • Graceful shutdown timing
    • Invalid intervalSeconds values
  2. SyncStateTracker:

    • Permission failures (read-only directories)
    • Malformed JSON handling
    • Race conditions in concurrent access
    • Offset reset logic (getStartingOffset)

Minor Improvements

Documentation

  • Consider adding usage examples in JSDoc
  • Document the offset reset logic in getStartingOffset:130-135

Consistency

  • Good that recent commits fixed resetState() to actually delete the file on disk
  • Good that in-memory cache update bug was fixed in recent commits

Recommendation: APPROVE

This is solid utility code that follows repo conventions and solves legitimate needs. The identified issues are minor and don't block the PR. The atomic file writes, graceful error handling, and proper TypeScript usage demonstrate good engineering practices.

Priority fixes for future PR:

  1. Add input validation for intervalSeconds
  2. Add temp file cleanup in SyncStateTracker
  3. Consider adding basic unit tests
  4. Evaluate JSON parsing type safety

View job

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/core/utils/index.ts`:
- Around line 3-4: The PR didn't run the required verification steps for the
exports in src/core/utils/index.ts (symbols: runContinuousLoop,
ContinuousLoopOptions, SyncStateTracker, SyncState, SyncStateTrackerOptions);
run the full suite locally: npm run lint && npm run build && npm test, fix any
lint/build/test failures that appear (adjust code or tests as needed), commit
the fixes, and update the PR with the test/lint/build results and any changes
made so reviewers can validate the pass.

Clear cached state and state file via resetState() when
lastSuccessfulOffset > currentLedgerEnd, so the reset detection
is durable even if the caller crashes before calling updateState().
@claude
Copy link

claude bot commented Feb 20, 2026

Claude finished @HardlyDifficult's task —— View job


Code Review Complete ✅

Todo List

  • Read and analyze all changed files
  • Review code quality and TypeScript best practices
  • Check for potential bugs and security issues
  • Evaluate performance considerations
  • Assess test coverage needs
  • Provide comprehensive feedback

Review Feedback

Overall Assessment

This PR introduces well-structured utility functions that appear to solve legitimate runtime needs for Canton scripts. The code follows TypeScript best practices and maintains good separation of concerns.

Code Quality & Best Practices ✅

Strengths:

  • Excellent TypeScript typing with proper interfaces and explicit return types
  • Good separation of concerns with focused, single-responsibility utilities
  • Consistent error handling patterns (graceful degradation in SyncStateTracker)
  • Proper resource cleanup in runContinuousLoop with signal handlers
  • Follows repo conventions from CLAUDE.md (explicit return types, no any, proper exports)

Potential Issues & Suggestions

1. Input Validation Missing (continuous-loop.ts:21)

const { intervalSeconds, runCycle, onShutdown, onCycleError } = options;

Issue: No validation for intervalSeconds. Invalid values (≤0, NaN, Infinity) passed to setTimeout at line 67 could cause hot loops.
Suggestion: Add validation at function start:

if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
  throw new Error('intervalSeconds must be a positive, finite number');
}

2. File System Race Conditions (sync-state-tracker.ts:116-118)

const tempPath = `${this.stateFilePath}.tmp`;
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2));
fs.renameSync(tempPath, this.stateFilePath);

Issue: Atomic writes are good, but there's no cleanup if renameSync fails.
Suggestion: Add try/catch around rename with temp file cleanup:

try {
  fs.renameSync(tempPath, this.stateFilePath);
} catch (error) {
  try { fs.unlinkSync(tempPath); } catch {}
  throw error;
}

3. Type Safety (sync-state-tracker.ts:81)

const state = JSON.parse(content) as SyncState;

Issue: as SyncState bypasses type checking. JSON parsing could return malformed data.
Suggestion: Use proper validation or zod schema validation (consistent with repo patterns).

4. Directory Write Check Logic (sync-state-tracker.ts:49-58)

let isWritable = true;
if (!fs.existsSync(stateDir)) {
  try {
    fs.mkdirSync(stateDir, { recursive: true });
  } catch {
    isWritable = false;
  }
}

Issue: If directory exists but lacks write permissions, isWritable stays true but writes will fail.
Suggestion: Add explicit write permission check after ensuring directory exists.

5. Signal Handler Cleanup (continuous-loop.ts:42-43)

The signal handlers are properly cleaned up, but consider edge cases:

  • Multiple calls to runContinuousLoop could register multiple handlers
  • Consider using once instead of on for signal handlers

6. Error Boundaries (continuous-loop.ts:55)

onCycleError?.(error);

Question: Should cycle errors stop the loop or continue? Current behavior continues, but this might not always be desired. Consider adding a stopOnError option.

Security Considerations ✅

  • File system operations: Properly handles permissions failures gracefully
  • Path traversal: Uses path.join() correctly, no user input concatenation
  • Signal handling: Clean signal handler registration/deregistration

Performance Considerations ✅

  • File I/O caching: SyncStateTracker caches state to avoid repeated file reads
  • Atomic writes: Temp file + rename pattern prevents corruption
  • Memory usage: No obvious leaks, proper cleanup in finally blocks
  • Timing: intervalSeconds * 1000 conversion is clear and correct

Test Coverage Gaps ⚠️

The utilities would benefit from unit tests covering:

  1. runContinuousLoop:

    • Signal handling (SIGINT/SIGTERM)
    • Error handling in cycles
    • Graceful shutdown timing
    • Invalid intervalSeconds values
  2. SyncStateTracker:

    • Permission failures (read-only directories)
    • Malformed JSON handling
    • Race conditions in concurrent access
    • Offset reset logic (getStartingOffset)

Minor Improvements

Documentation

  • Consider adding usage examples in JSDoc
  • Document the offset reset logic in getStartingOffset:130-135

Consistency

  • Good that recent commits fixed resetState() to actually delete the file on disk
  • Good that in-memory cache update bug was fixed in recent commits

Recommendation: APPROVE

This is solid utility code that follows repo conventions and solves legitimate needs. The identified issues are minor and don't block the PR. The atomic file writes, graceful error handling, and proper TypeScript usage demonstrate good engineering practices.

Priority fixes for future PR:

  1. Add input validation for intervalSeconds
  2. Add temp file cleanup in SyncStateTracker
  3. Consider adding basic unit tests
  4. Evaluate JSON parsing type safety

View job

@cursor

This comment has been minimized.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/utils/sync-state-tracker.ts`:
- Around line 83-89: The current validation in the sync-state-tracker is only
checking types but not that offsets are non-negative integers; update the guard
in the function that reads state (used by getStartingOffset) to ensure
state.lastSuccessfulOffset and state.ledgerEndAtSync pass Number.isInteger(...)
and are >= 0 (in addition to being numbers), and keep the existing check for
state.lastUpdated being a string; if either offset fails these non-negative
integer checks, return null so getStartingOffset won't receive negative or
fractional offsets.

---

Duplicate comments:
In `@src/core/utils/sync-state-tracker.ts`:
- Around line 46-60: The constructor in SyncStateTracker currently sets
directoryWritable based only on mkdir success and builds stateFilePath by
interpolating options.network/options.provider directly; update it to explicitly
verify write permission (use fs.access or fs.accessSync with fs.constants.W_OK)
against the resolved stateDir after ensuring the directory exists and set
directoryWritable accordingly, and sanitize the network and provider inputs
before joining into stateFilePath (e.g., allow only a safe whitelist/regex like
alphanumeric, dash, underscore or use path.basename to strip traversal) so that
stateFilePath = path.join(resolvedStateDir,
`${sanitizedNetwork}-${sanitizedProvider}.json`); reference the SyncStateTracker
constructor, directoryWritable, stateFilePath, options.network and
options.provider when making these changes.

Comment on lines +83 to +89
if (
typeof state.lastSuccessfulOffset !== 'number' ||
typeof state.ledgerEndAtSync !== 'number' ||
typeof state.lastUpdated !== 'string'
) {
return null;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider validating non-negative integers.

The validation checks types but doesn't verify that lastSuccessfulOffset and ledgerEndAtSync are non-negative integers. Negative or fractional values could cause unexpected behavior in getStartingOffset.

🛡️ Optional enhancement
       if (
         typeof state.lastSuccessfulOffset !== 'number' ||
         typeof state.ledgerEndAtSync !== 'number' ||
-        typeof state.lastUpdated !== 'string'
+        typeof state.lastUpdated !== 'string' ||
+        !Number.isInteger(state.lastSuccessfulOffset) ||
+        !Number.isInteger(state.ledgerEndAtSync) ||
+        state.lastSuccessfulOffset < 0 ||
+        state.ledgerEndAtSync < 0
       ) {
         return null;
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
typeof state.lastSuccessfulOffset !== 'number' ||
typeof state.ledgerEndAtSync !== 'number' ||
typeof state.lastUpdated !== 'string'
) {
return null;
}
if (
typeof state.lastSuccessfulOffset !== 'number' ||
typeof state.ledgerEndAtSync !== 'number' ||
typeof state.lastUpdated !== 'string' ||
!Number.isInteger(state.lastSuccessfulOffset) ||
!Number.isInteger(state.ledgerEndAtSync) ||
state.lastSuccessfulOffset < 0 ||
state.ledgerEndAtSync < 0
) {
return null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/utils/sync-state-tracker.ts` around lines 83 - 89, The current
validation in the sync-state-tracker is only checking types but not that offsets
are non-negative integers; update the guard in the function that reads state
(used by getStartingOffset) to ensure state.lastSuccessfulOffset and
state.ledgerEndAtSync pass Number.isInteger(...) and are >= 0 (in addition to
being numbers), and keep the existing check for state.lastUpdated being a
string; if either offset fails these non-negative integer checks, return null so
getStartingOffset won't receive negative or fractional offsets.

Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON. A Cloud Agent has been kicked off to fix the reported issue.

If the user-provided onCycleError handler throws, the exception would
propagate out of the catch block and terminate the continuous loop.
Wrapping it in its own try-catch ensures the loop remains resilient.
@cursor
Copy link
Contributor

cursor bot commented Feb 20, 2026

Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.

  • ✅ Fixed: Throwing error callback terminates the continuous loop
    • Wrapped the onCycleError callback invocation in its own try-catch so that a throwing error handler no longer propagates out of the catch block and terminates the loop.

@claude
Copy link

claude bot commented Feb 20, 2026

Claude finished @HardlyDifficult's task —— View job


Code Review Complete ✅

Todo List

  • Read and analyze all changed files
  • Review code quality and TypeScript best practices
  • Check for potential bugs and security issues
  • Evaluate performance considerations
  • Assess test coverage needs
  • Provide comprehensive feedback

Review Feedback

Overall Assessment

This PR introduces well-structured utility functions that appear to solve legitimate runtime needs for Canton scripts. The code follows TypeScript best practices and maintains good separation of concerns.

Code Quality & Best Practices ✅

Strengths:

  • Excellent TypeScript typing with proper interfaces and explicit return types
  • Good separation of concerns with focused, single-responsibility utilities
  • Consistent error handling patterns (graceful degradation in SyncStateTracker)
  • Proper resource cleanup in runContinuousLoop with signal handlers
  • Follows repo conventions from CLAUDE.md (explicit return types, no any, proper exports)

Potential Issues & Suggestions

1. Input Validation Missing (continuous-loop.ts:21)

const { intervalSeconds, runCycle, onShutdown, onCycleError } = options;

Issue: No validation for intervalSeconds. Invalid values (≤0, NaN, Infinity) passed to setTimeout at line 67 could cause hot loops.
Suggestion: Add validation at function start:

if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
  throw new Error('intervalSeconds must be a positive, finite number');
}

2. File System Race Conditions (sync-state-tracker.ts:116-118)

const tempPath = `${this.stateFilePath}.tmp`;
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2));
fs.renameSync(tempPath, this.stateFilePath);

Issue: Atomic writes are good, but there's no cleanup if renameSync fails.
Suggestion: Add try/catch around rename with temp file cleanup:

try {
  fs.renameSync(tempPath, this.stateFilePath);
} catch (error) {
  try { fs.unlinkSync(tempPath); } catch {}
  throw error;
}

3. Type Safety (sync-state-tracker.ts:81)

const state = JSON.parse(content) as SyncState;

Issue: as SyncState bypasses type checking. JSON parsing could return malformed data.
Suggestion: Use proper validation or zod schema validation (consistent with repo patterns).

4. Directory Write Check Logic (sync-state-tracker.ts:49-58)

let isWritable = true;
if (!fs.existsSync(stateDir)) {
  try {
    fs.mkdirSync(stateDir, { recursive: true });
  } catch {
    isWritable = false;
  }
}

Issue: If directory exists but lacks write permissions, isWritable stays true but writes will fail.
Suggestion: Add explicit write permission check after ensuring directory exists.

5. Signal Handler Cleanup (continuous-loop.ts:42-43)

The signal handlers are properly cleaned up, but consider edge cases:

  • Multiple calls to runContinuousLoop could register multiple handlers
  • Consider using once instead of on for signal handlers

6. Error Boundaries (continuous-loop.ts:55)

onCycleError?.(error);

Question: Should cycle errors stop the loop or continue? Current behavior continues, but this might not always be desired. Consider adding a stopOnError option.

Security Considerations ✅

  • File system operations: Properly handles permissions failures gracefully
  • Path traversal: Uses path.join() correctly, no user input concatenation
  • Signal handling: Clean signal handler registration/deregistration

Performance Considerations ✅

  • File I/O caching: SyncStateTracker caches state to avoid repeated file reads
  • Atomic writes: Temp file + rename pattern prevents corruption
  • Memory usage: No obvious leaks, proper cleanup in finally blocks
  • Timing: intervalSeconds * 1000 conversion is clear and correct

Test Coverage Gaps ⚠️

The utilities would benefit from unit tests covering:

  1. runContinuousLoop:

    • Signal handling (SIGINT/SIGTERM)
    • Error handling in cycles
    • Graceful shutdown timing
    • Invalid intervalSeconds values
  2. SyncStateTracker:

    • Permission failures (read-only directories)
    • Malformed JSON handling
    • Race conditions in concurrent access
    • Offset reset logic (getStartingOffset)

Minor Improvements

Documentation

  • Consider adding usage examples in JSDoc
  • Document the offset reset logic in getStartingOffset:130-135

Consistency

  • Good that recent commits fixed resetState() to actually delete the file on disk
  • Good that in-memory cache update bug was fixed in recent commits

Recommendation: APPROVE

This is solid utility code that follows repo conventions and solves legitimate needs. The identified issues are minor and don't block the PR. The atomic file writes, graceful error handling, and proper TypeScript usage demonstrate good engineering practices.

Priority fixes for future PR:

  1. Add input validation for intervalSeconds
  2. Add temp file cleanup in SyncStateTracker
  3. Consider adding basic unit tests
  4. Evaluate JSON parsing type safety

View job

@HardlyDifficult
Copy link
Collaborator Author

Superseded by upgraded @hardlydifficult/daemon and @hardlydifficult/state-tracker packages. We no longer need to carry these runtime utilities in canton-node-sdk.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants